fix(lexer): stop dropping input after an unlexable byte - #3015
Open
dkxmercury wants to merge 1 commit into
Open
Conversation
In `Lexer::lex`, when `lex_match` stopped at a byte that no matcher covers, the fallback ran the last-resort matcher on the original `str_buff` instead of the unlexed remainder `res.forward_string`, and broke out of the loop as soon as it produced any element. Since the last-resort pattern always matches (it can match the empty string), the loop always broke and the "advance and append" lines were dead code. The result was silent data loss: everything from the first unlexable byte to the end of input was discarded. For example `select ф` lexed to just `select `, so `fix` would silently delete part of the user's SQL and `lint` parsed a truncated statement. Run the last-resort matcher on the remainder, append its elements, and break only when it makes no progress (to avoid an infinite loop). The unlexable run now becomes an `Unlexable` segment and lexing is lossless.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Lexer::lexsilently drops all input from the first unlexable byte to the end of the string, sofixcan delete part of the user's SQL andlintparses a truncated statement.Root cause
In the lex loop, when
lex_matchstops at a byte that no matcher covers, the fallback is wired to the wrong string and always breaks:The last-resort matcher (
[^\t\n.]*) can match the empty string, soresort_res.elementsis never empty and the loop always breaks before reaching the "advance and append" lines. Whateverlex_matchcould not consume is discarded.Concretely,
select фlexes to justselect(the non-ASCII identifier is dropped). This does not panic, because the dropped bytes are always a clean suffix, but for a formatter it is silent data loss.Fix
Run the last-resort matcher on the unlexed remainder
res.forward_string, append its elements, advancestr_buff, and break only when it makes no progress (guarding against an infinite loop). The unlexable run then becomes anUnlexablesegment and lexing is lossless. Fully lexable input is unaffected, since the fallback only runs whenlex_matchhits a byte it cannot cover.Testing
Added
lex_preserves_unlexable_input, which lexesselect фthroughLexer::lexand asserts the segments round-trip to the original text. It fails onmain(left: "select ",right: "select ф") and passes with the fix.cargo test -p sqruff-lib-coreis green (all tests) andcargo clippy -p sqruff-lib-core --testsis clean.One honest note on my local run: the
lib-dialectsdialectsfixture test reports a couple ofexpect test failedmismatches in my Windows checkout, involving\r\nin Snowflakeudf_bodyfixtures. They are present on a cleanmaintoo (a CRLF/line-ending artifact of checking out on Windows) and this change does not add any; the Linux CI should be the real check.